package cn.remex.configuration.spring;

import cn.remex.configuration.etcd.EtcdConfigCenter;
import cn.remex.core.util.Judgment;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.*;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringValueResolver;

import java.util.*;

/**
 * Disc: BeanDefinition处理占位符的类，并根据情况进行etcd的查询和监控变化
 * 第一阶段ETCD部分只处理String类型的property
 * Created by xinxinran on 2017/7/25.
 */
class BeanDefinitionEtcdWatchVisitor {


    private StringValueResolver valueResolver;

    //private final String WATCH_PREFIX;

    private final boolean canProcessWatch;

    private BeanFactory beanFactory;



    BeanDefinitionEtcdWatchVisitor(BeanFactory beanFactory, StringValueResolver valueResolver, String etcdWatchPrefix) {
        Assert.notNull(valueResolver, "StringValueResolver must not be null");
        this.beanFactory = beanFactory;
        this.valueResolver = valueResolver;
        //this.WATCH_PREFIX = etcdWatchPrefix;
        this.canProcessWatch = !Judgment.nullOrBlank(etcdWatchPrefix);
    }


    void visitBeanDefinition(BeanDefinition beanDefinition) {
        visitParentName(beanDefinition);
        visitBeanClassName(beanDefinition);
        visitFactoryBeanName(beanDefinition);
        visitFactoryMethodName(beanDefinition);
        visitScope(beanDefinition);
        visitPropertyValues(beanDefinition);
        ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
        visitIndexedArgumentValues(cas.getIndexedArgumentValues());
        visitGenericArgumentValues(cas.getGenericArgumentValues());
    }

    private void visitParentName(BeanDefinition beanDefinition) {
        String parentName = beanDefinition.getParentName();
        if (parentName != null) {
            String resolvedName = resolveStringValue(parentName).getValue();
            if (!parentName.equals(resolvedName)) {
                beanDefinition.setParentName(resolvedName);
            }
        }

    }

    private void visitBeanClassName(BeanDefinition beanDefinition) {
        String beanClassName = beanDefinition.getBeanClassName();
        if (beanClassName != null) {
            String resolvedName = resolveStringValue(beanClassName).getValue();
            if (!beanClassName.equals(resolvedName)) {
                beanDefinition.setBeanClassName(resolvedName);
            }
        }
    }

    private void visitFactoryBeanName(BeanDefinition beanDefinition) {
        String factoryBeanName = beanDefinition.getFactoryBeanName();
        if (factoryBeanName != null) {
            String resolvedName = resolveStringValue(factoryBeanName).getValue();
            if (!factoryBeanName.equals(resolvedName)) {
                beanDefinition.setFactoryBeanName(resolvedName);
            }
        }
    }

    private void visitFactoryMethodName(BeanDefinition beanDefinition) {
        String factoryMethodName = beanDefinition.getFactoryMethodName();
        if (factoryMethodName != null) {
            String resolvedName = resolveStringValue(factoryMethodName).getValue();
            if (!factoryMethodName.equals(resolvedName)) {
                beanDefinition.setFactoryMethodName(resolvedName);
            }
        }
    }

    private void visitScope(BeanDefinition beanDefinition) {
        String scope = beanDefinition.getScope();
        if (scope != null) {
            String resolvedScope = resolveStringValue(scope).getValue();
            if (!scope.equals(resolvedScope)) {
                beanDefinition.setScope(resolvedScope);
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void visitPropertyValues(final BeanDefinition beanDefinition) {
        MutablePropertyValues pvs = beanDefinition.getPropertyValues();
        PropertyValue[] pvArray = pvs.getPropertyValues();
        for (PropertyValue pv : pvArray) {

            ValueWithWatchable newValWatchable = resolveValue(pv.getValue());
            Object newVal = newValWatchable.getValue();
            if (!ObjectUtils.nullSafeEquals(newVal, pv.getValue())) {
                pvs.add(pv.getName(), newVal);
            }

            if(canProcessWatch && newValWatchable.needWatch){
                List<String> keys = newValWatchable.getWatchedKeys();
                keys.forEach(key -> EtcdConfigCenter.registerWatching(key, changedValue -> {
                    pvs.add(pv.getName(), changedValue);

                    if(beanDefinition.isSingleton()){
                        Object bean = beanFactory.getBean(beanDefinition.getFactoryBeanName());
                        EtcdConfigCenter.setBeanProperty(bean, pv.getName(), changedValue);
                    }
                }));
            }
        }
    }

    private void visitIndexedArgumentValues(Map<Integer, ConstructorArgumentValues.ValueHolder> ias) {
        for (ConstructorArgumentValues.ValueHolder valueHolder : ias.values()) {
            Object newVal = resolveValue(valueHolder.getValue()).getValue();
            if (!ObjectUtils.nullSafeEquals(newVal, valueHolder.getValue())) {
                valueHolder.setValue(newVal);
            }
        }
    }

    private void visitGenericArgumentValues(List<ConstructorArgumentValues.ValueHolder> gas) {
        for (ConstructorArgumentValues.ValueHolder valueHolder : gas) {
            Object newVal = resolveValue(valueHolder.getValue()).getValue();
            if (!ObjectUtils.nullSafeEquals(newVal, valueHolder.getValue())) {
                valueHolder.setValue(newVal);
            }
        }
    }

    @SuppressWarnings("rawtypes")
    private ValueWithWatchable resolveValue(Object value) {
        if (value instanceof BeanDefinition) {
            visitBeanDefinition((BeanDefinition) value);
        }
        else if (value instanceof BeanDefinitionHolder) {
            visitBeanDefinition(((BeanDefinitionHolder) value).getBeanDefinition());
        }
        else if (value instanceof RuntimeBeanReference) {
            RuntimeBeanReference ref = (RuntimeBeanReference) value;
            String newBeanName = resolveStringValue(ref.getBeanName()).getValue();
            if (!newBeanName.equals(ref.getBeanName())) {
                return new ValueWithWatchable<>(new RuntimeBeanReference(newBeanName));
            }
        }
        else if (value instanceof RuntimeBeanNameReference) {
            RuntimeBeanNameReference ref = (RuntimeBeanNameReference) value;
            String newBeanName = resolveStringValue(ref.getBeanName()).getValue();
            if (!newBeanName.equals(ref.getBeanName())) {
                return new ValueWithWatchable<>(new RuntimeBeanNameReference(newBeanName));
            }
        }
        else if (value instanceof Object[]) {
            visitArray((Object[]) value);
        }
        else if (value instanceof List) {
            visitList((List) value);
        }
        else if (value instanceof Set) {
            visitSet((Set) value);
        }
        else if (value instanceof Map) {
            visitMap((Map) value);
        }
        else if (value instanceof TypedStringValue) {
            TypedStringValue typedStringValue = (TypedStringValue) value;
            String stringValue = typedStringValue.getValue();
            if (stringValue != null) {
                String visitedString = resolveStringValue(stringValue).getValue();
                typedStringValue.setValue(visitedString);
            }
        }
        else if (value instanceof String) {
            return resolveStringValue((String) value);
        }
        return new ValueWithWatchable<>(value);  //TODO 第一阶段只处理String类型的属性
    }

    private void visitArray(Object[] arrayVal) {
        for (int i = 0; i < arrayVal.length; i++) {
            Object elem = arrayVal[i];
            Object newVal = resolveValue(elem).getValue();
            if (!ObjectUtils.nullSafeEquals(newVal, elem)) {
                arrayVal[i] = newVal;
            }
        }
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private void visitList(List listVal) {
        for (int i = 0; i < listVal.size(); i++) {
            Object elem = listVal.get(i);
            Object newVal = resolveValue(elem).getValue();
            if (!ObjectUtils.nullSafeEquals(newVal, elem)) {
                listVal.set(i, newVal);
            }
        }
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private void visitSet(Set setVal) {
        Set newContent = new LinkedHashSet();
        boolean entriesModified = false;
        for (Object elem : setVal) {
            int elemHash = (elem != null ? elem.hashCode() : 0);
            Object newVal = resolveValue(elem).getValue();
            int newValHash = (newVal != null ? newVal.hashCode() : 0);
            newContent.add(newVal);
            entriesModified = entriesModified || (newVal != elem || newValHash != elemHash);
        }
        if (entriesModified) {
            setVal.clear();
            setVal.addAll(newContent);
        }
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private void visitMap(Map<?, ?> mapVal) {
        Map newContent = new LinkedHashMap();
        boolean entriesModified = false;
        for (Map.Entry entry : mapVal.entrySet()) {
            Object key = entry.getKey();
            int keyHash = (key != null ? key.hashCode() : 0);
            Object newKey = resolveValue(key).getValue();
            int newKeyHash = (newKey != null ? newKey.hashCode() : 0);
            Object val = entry.getValue();
            Object newVal = resolveValue(val).getValue();
            newContent.put(newKey, newVal);
            entriesModified = entriesModified || (newVal != val || newKey != key || newKeyHash != keyHash);
        }
        if (entriesModified) {
            mapVal.clear();
            mapVal.putAll(newContent);
        }
    }

    /**
     * 处理字符串，当字符串为placeholder时，进行自动匹配
     * 当结果字符串为etcd://开头时，进行etcd查询处理（循环进行直到拿到最终值）
     *
     * @param strVal 原字符串
     * @return 处理后的字符串／是否需要watch的对象
     *
     * @see ValueWithWatchable
     */
    private ValueWithWatchable<String> resolveStringValue(String strVal) {
        if (this.valueResolver == null) {
            throw new IllegalStateException("No StringValueResolver specified - pass a resolver " +
                    "object into the constructor or override the 'resolveStringValue' method");
        }

        List<String> toAddKeys = EtcdConfigCenter.getEtcdKeysFromPlaceHolder(strVal);
        String resolvedValue = this.valueResolver.resolveStringValue(strVal);

        while(EtcdConfigCenter.isEtcdKey(resolvedValue)){
            toAddKeys.add(resolvedValue);
            resolvedValue = this.valueResolver.resolveStringValue("${" + resolvedValue + "}");
        }
        // Return original String if not modified.
        ValueWithWatchable<String> result = new ValueWithWatchable<>(strVal.equals(resolvedValue) ? strVal : resolvedValue);
        toAddKeys.forEach(result::addMaybeWatchedKey);
        return result;
    }

    private class ValueWithWatchable<T>{
        private T value;
        private boolean needWatch = false;
        private List<String> watchedKeys = null;
        ValueWithWatchable(T value){
            this.value = value;
        }

        void addMaybeWatchedKey(String key){
            if(canProcessWatch && EtcdConfigCenter.canWatchEtcdKey(key)){
                needWatch = true;
                if(watchedKeys == null) watchedKeys = new ArrayList<>(3);
                watchedKeys.add(key);
            }
        }

        public T getValue() {
            return value;
        }

        @SuppressWarnings("unused")
        public boolean isNeedWatch() {
            return needWatch;
        }

        List<String> getWatchedKeys() {
            return watchedKeys;
        }
    }
}
